package apibasej.db.sql.wrapper;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

import apibasej.db.metadata.Col;

public class BlockWhere {
	
	private static final String AND = "AND";
	private static final String OR = "OR";
	
	private static final String IN = "IN";
	private static final String NOT_IN = "NOT IN";
	private static final String IS_NOT_NULL = "IS NOT NULL";
	private static final String IS_NULL = "IS NULL";
	
	
	private ArrayList<Object> partsInBlock = new ArrayList<>(); // BlockWhere e Filters (e StrConcatWhere)
	
	private String operatorBetweenPartsInBlock = null; //!!! setar AND no BlockWhere principal (root) !!!

	private String strBlock = null; //somente para caso queira setar a string do bloco, neste caso irá ignorar a geração e as partsInBlock
	
	private WrapperSQL wrapperSQL;
	
	private BlockWhere(String operatorBetweenPartsInBlock, WrapperSQL wrapperSQL) {
		this.operatorBetweenPartsInBlock = operatorBetweenPartsInBlock;
		this.wrapperSQL = wrapperSQL;
	}
	
	//-----------------------------------------------
	static BlockWhere newBlockAND(WrapperSQL wrapperSQL){
		return new BlockWhere(AND,wrapperSQL);
	}
	static BlockWhere newBlockOR(WrapperSQL wrapperSQL){
		return new BlockWhere(OR,wrapperSQL);
	}
	//--------------
	public BlockWhere addNewBlockChildAND(){
		BlockWhere b = newBlockAND(wrapperSQL);
		addBlock(b);
		return b;
	}
	public BlockWhere addNewBlockChildOR(){
		BlockWhere b = newBlockOR(wrapperSQL);
		addBlock(b);
		return b;
	}
	//---------------------------------------
	public void addFilter(Filter<?> f){
		partsInBlock.add(f);
	}
	private void addBlock(BlockWhere b){
		partsInBlock.add(b);
	}
	//--------------------------
	public String getStrBlock(){
		if(strBlock!=null){
			return strBlock;
		}else{
			StringBuilder sb = new StringBuilder("");
			int ctParts = 0;
			for(Object part : partsInBlock){
				String strToApp = null;
				if(part instanceof Filter<?>){
					strToApp = ((Filter<?>)part).getStrForWhere();
				}else if(part instanceof BlockWhere){
					strToApp = ((BlockWhere)part).getStrBlock();
				}else {
					//não deve ocorrer
					throw new IllegalStateException("Invalid part of block: "+part);
				}
				boolean hasStrToApp = strToApp!=null && strToApp.trim().length()>0;
				if(hasStrToApp){
					if(ctParts>0){//só coloca o operador a esquerda a partir do segundo item do bloco
						sb.append(' ');
						sb.append(operatorBetweenPartsInBlock);
						sb.append(' ');
					}
					sb.append(strToApp);
					ctParts++;
				}
			}
			if(ctParts>1){//só precisa dos parênteses no bloco se tiver mais de uma parte no bloco
				sb.insert(0,'(');
				sb.append(')');
			}
			return sb.toString();
		}
	}
	
	
	public void setStrBlock(String strBlock) {
		this.strBlock = strBlock;
	}
	
	public String getOperatorBetweenPartsInBlock() {
		return operatorBetweenPartsInBlock;
	}
	public boolean isOperatorBetweenPartsInBlockAND(){
		return AND.equalsIgnoreCase(getOperatorBetweenPartsInBlock().trim());
	}
	public void validateOperatorBetweenPartsInBlockAND() throws SQLWrapperException{
		if(!isOperatorBetweenPartsInBlockAND()){
			throw new SQLWrapperException("This BlockWhere must have operator AND! (op="+getOperatorBetweenPartsInBlock()+")");
		}
	}
	/* não permitir mudar! para ter segurança que não irá trocar o operador do bloco main!
	 * se quiser usar OR no main criar um bloco somente pera este OR
	 * se depois puder setar, validar para não setar no where root
	public void setOperatorBetweenPartsInBlockAND() {//chamar somente se quiser trocar o operador de um bloco já criado
		this.operatorBetweenPartsInBlock = AND;
	}
	public void setOperatorBetweenPartsInBlockOR() {//chamar somente se quiser trocar o operador de um bloco já criado (como o root que cria por padrão com AND)
		this.operatorBetweenPartsInBlock = OR;
	}*/
	
	public ArrayList<Filter<?>> getAllChildFilters(){
		ArrayList<Filter<?>> filters = new ArrayList<>();
		addFilters(filters, this);
		return filters;
	}
	private void addFilters(ArrayList<Filter<?>> filters, BlockWhere b){
		for(Object part : b.getPartsInBlock()){
			if(part instanceof Filter<?>){
				filters.add((Filter<?>)part);
			}else if(part instanceof BlockWhere){
				addFilters(filters, (BlockWhere)part);
			}else {
				//não deve ocorrer
				throw new IllegalStateException("Invalid part of block: "+part);
			}
		}
	}
	private ArrayList<Object> getPartsInBlock() {
		return partsInBlock;
	}

	//---------------------------------
	public Filter<?> addFilterUnchecked(Col<?> col, Object val) throws SQLWrapperException{
		return addFilterUnchecked(col, val, getWrapperSQL().getWrapperTabAddedForCol(col));
	}
	public <T> Filter<T> addFilter(Col<T> col, T val, WrapperTab tabForCol) throws SQLWrapperException{
		if(tabForCol==null){
			tabForCol = getWrapperSQL().getWrapperTabAddedForCol(col);
		}
		Filter<T> filter = new Filter<T>(tabForCol, col, val);
		addFilter(filter);
		return filter;
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public Filter<?> addFilterUnchecked(Col<?> col, Object val, WrapperTab tabForCol){
		Filter<?> filter = new Filter(tabForCol, col, val);
		addFilter(filter);
		return filter;
	}
	
	@SuppressWarnings("unchecked")
	public <T> Filter<T> addFilterIn(Col<T> col, T... vals) throws SQLWrapperException{
		return addFilterIn(col, Arrays.asList(vals));
	}
	public <T> Filter<T> addFilterIn(Col<T> col, Collection<T> vals) throws SQLWrapperException{
		return addFilterIn(col, vals, false, null, null);
	}
	public <T> Filter<T> addFilterNotIn(Col<T> col, Collection<T> vals) throws SQLWrapperException{
		return addFilterIn(col, vals, true, null, null);
	}
	public <T> Filter<T> addFilterIn(Col<T> col, Collection<T> vals, WrapperTab wt) throws SQLWrapperException{
		return addFilterIn(col, vals, false, wt, null);
	}
	public <T> Filter<T> addFilterNotIn(Col<T> col, Collection<T> vals, WrapperTab wt) throws SQLWrapperException{
		return addFilterIn(col, vals, true, wt, null);
	}
	
	public <T> Filter<T> addFilterIn(Col<T> col, WrapperSQL wrapperSqlSubSelect) throws SQLWrapperException{
		return addFilterIn(col, null, false, null, wrapperSqlSubSelect);
	}
	//public <T> Filter<T> addFilterIn(Col<T> col, WrapperSQL wrapperSqlSubSelect, boolean isNotIn) throws SQLWrapperException{
	//	return addFilterIn(col, null, isNotIn, null, wrapperSqlSubSelect);
	//}
	public <T> Filter<T> addFilterNotIn(Col<T> col, WrapperSQL wrapperSqlSubSelect) throws SQLWrapperException{
		return addFilterIn(col, null, true, null, wrapperSqlSubSelect);
	}
	public <T> Filter<T> addFilterIn(Col<T> col, WrapperSQL wrapperSqlSubSelect, WrapperTab wt) throws SQLWrapperException{
		return addFilterIn(col, null, false, wt, wrapperSqlSubSelect);
	}
	public <T> Filter<T> addFilterNotIn(Col<T> col, WrapperSQL wrapperSqlSubSelect, WrapperTab wt) throws SQLWrapperException{
		return addFilterIn(col, null, true, wt, wrapperSqlSubSelect);
	}
	
	// colocado esta verificação de Number 
	//somente para não tentar com Strings ou outros valores que não funcionam com v.toString() ...
	private <T> Filter<T> addFilterIn(Col<T> col, Collection<T> vals, boolean isNotIn, WrapperTab wt, WrapperSQL wrapperSqlSubSelect) throws SQLWrapperException{
		if( (vals==null || vals.size()<=0) && wrapperSqlSubSelect==null){
			throw new SQLWrapperException("No value received");
		}
		StringBuilder sbIn = new StringBuilder();
		
		Filter<T> e = addFilter(col, null, wt);
		e.setOperatorForValue(isNotIn?NOT_IN:IN);
		if(wrapperSqlSubSelect!=null){
			sbIn.append(wrapperSqlSubSelect.getSqlSelect());
			ArrayList<FilterValue> fvs = new ArrayList<FilterValue>();
			for(Filter<?> f : wrapperSqlSubSelect.getBlockWhere().getAllChildFilters()){
				if(f.ifSetParamInQuery()){//só adiciona FilterValue se for preciso setar parametro na query ...
					fvs.add(new FilterValue(f.getCol()!=null?f.getCol().getType():null, f.getValue()));//deve permitir valores null, pois o filtro pode não ser valor como NOT NULL ...					
				}
			}
			e.setValues(fvs);
			sbIn.insert(0, '(');//insere ( no início
			sbIn.append(')');
		}else{
			e.setValuesSameType(vals);
			for(T v : vals){
				if(v==null){
					throw new SQLWrapperException("Value cannot be null!");
				}
				sbIn.append(",?");
			}
			sbIn.setCharAt(0, '(');//substitui a primeira virgula por (
			sbIn.append(')');
		}
		e.setStrFilterAfterOperator(sbIn.toString());
		return e;
	}
	
	
	public <T> Filter<T> addFilterIgnoreCase(Col<T> col, T val) throws SQLWrapperException{
		Filter<T> f = addFilter(col, val, null);
		f.setStrFilterBeforeOperator("UPPER("+f.getStrFilterBeforeOperator()+")");
		f.setStrFilterAfterOperator("UPPER("+f.getStrFilterAfterOperator()+")");
		return f;
	}
	public <T> Filter<T> addFilter(Col<T> col, T val) throws SQLWrapperException{
		return addFilter(col, val, null);
	}
	public <T> Filter<T> addFilterAllowingNullValue(Col<T> col, T val) throws SQLWrapperException{
		if(val!=null){
			return addFilter(col, val);
		}else{
			return addFilterIsNull(col);
		}
	}
	public <T> Filter<T> addFilterIsNotNull(Col<T> col) throws SQLWrapperException{
		Filter<T> e = addFilter(col, null);
		e.setOperatorForValue(IS_NOT_NULL);
		e.setStrFilterAfterOperator("");
		return e;
	}
	public <T> Filter<T> addFilterIsNull(Col<T> col) throws SQLWrapperException{
		Filter<T> e = addFilter(col, null);
		e.setOperatorForValue(IS_NULL);
		e.setStrFilterAfterOperator("");
		return e;
	}
	public void addFilterFalseOrNull(Col<Boolean> col) throws SQLWrapperException{
		addFilterFalseOrNull(col, null);
	}
	public void addFilterFalseOrNull(Col<Boolean> col, WrapperTab tabForCol) throws SQLWrapperException{
		
		//??? pode mudar e colocar o filtro diferente de true??? 
		//!!! Não pode! pois não retorna os registros null se colocar diferente de true!
		
		Filter<Boolean> e = addFilter(col, false, tabForCol);
		String aliasTabAndCol = e.getStrFilterBeforeOperator();
		e.setStrFilterBeforeOperator("("+aliasTabAndCol);
		e.setStrFilterAfterOperator("? "+OR+" "+aliasTabAndCol+" "+IS_NULL+")");
	}
	//-------------------------------------------------------------------------------
	
	public WrapperSQL getWrapperSQL() {
		return wrapperSQL;
	}
	
}
